feat: chat onboarding and routine advisor#927
Conversation
Port the complete psychographic profiling system from NPA into IronClaw, including enriched profile schema, conversational onboarding, profile evolution, and three-tier prompt augmentation. Personal onboarding moved from wizard Step 9 to first assistant interaction per maintainer feedback — the First Contact system prompt block now instructs the LLM to conduct a natural onboarding conversation that builds the psychographic profile via memory_write. Changes: - Enrich profile.rs with 5 new structs, 9-dimension analysis framework, custom deserializers for backward compatibility, and rendering methods - Add conversational onboarding engine with one-step-removed questioning technique, personality framework, and confidence-scored profile generation - Add profile evolution with confidence gating, analysis metadata tracking, and weekly update routine - Replace thin interaction style injection with three-tier system gated on confidence > 0.6 and profile recency - Replace wizard Step 9 with First Contact system prompt block that drives conversational onboarding during the user's first interaction - Add autonomy progression to SOUL.md seed and personality framework to AGENTS.md seed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pace seeds Remove the interactive onboarding_chat.rs engine in favor of a simpler bootstrap flow: fresh workspaces get a proactive LLM greeting that naturally profiles the user. Identity files are now seeded from src/workspace/seeds/ instead of being hardcoded. Also removes the identity-file write protection (seeds are now managed), adds routine advisor integration, and includes an e2e trace for bootstrap greeting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors the agent's initial user experience and introduces advanced personalization capabilities. It moves away from a rigid interactive onboarding flow to a more natural, conversational bootstrap greeting for new users, leveraging a new psychographic profiling system to understand and adapt to user preferences. Furthermore, it integrates routine management improvements, allowing for event-driven automation and continuous profile evolution, while simplifying memory write operations by centralizing seed file management. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant feature update, replacing the previous onboarding system with a dynamic, conversational bootstrap flow and introducing a comprehensive psychographic profiling system. While these features enhance user experience, they introduce significant prompt injection risks. The most critical issue is the removal of write protection for core identity files in the memory_write tool, which allows the agent to overwrite its own instructions. Additionally, untrusted user data is injected into prompts during profile evolution and personalization without sufficient sanitization, creating paths for both direct and indirect prompt injection. On the development side, the changes are well-structured, with notable improvements like moving hardcoded workspace seed files into a dedicated src/workspace/seeds/ directory, enhancing maintainability. A minor suggestion is to improve the consistency of cron expression examples in the new routine-advisor skill documentation to align with the updated tool documentation.
I am having trouble creating individual review comments. Click here to see my feedback.
src/tools/builtin/memory.rs (23-27)
The removal of PROTECTED_IDENTITY_FILES write protection is a critical security regression. These files (IDENTITY.md, SOUL.md, AGENTS.md, USER.md) are injected into the system prompt to define the agent's core behavior and boundaries. Allowing the LLM to overwrite them enables persistent prompt injection attacks where an attacker can permanently alter the agent's instructions, bypass safety guardrails, and gain full control over the agent in future sessions. Please restore the write protection for these files.
src/setup/profile_evolution.rs (28-29)
The recent_messages_summary is directly concatenated into the LLM prompt without sanitization or delimiters. Since this summary contains content from user messages, it can be used for prompt injection to manipulate the profile evolution process. An attacker could trick the LLM into generating a malicious psychographic profile. Recommend wrapping the summary in unique delimiters and instructing the LLM to treat it as untrusted data.
References
- Sanitization should be applied to data paths sent to external services, such as an LLM, to prevent prompt injection. User messages concatenated into LLM prompts without sanitization or delimiters can be used for prompt injection.
src/workspace/mod.rs (706-782)
Profile fields such as tone, goals, and pain_points are rendered directly into the system prompt. These fields are populated by the LLM and can be influenced by user input. If an attacker poisons these fields (e.g., via the evolution prompt), malicious instructions will be persistently injected into the system prompt of every session. Recommend implementing strict validation and sanitization for all profile fields before they are used in prompt construction.
References
- Sanitization should be applied to data paths sent to external services, such as an LLM, to prevent prompt injection. Profile fields influenced by user input and populated by an LLM should be validated and sanitized before use in prompt construction to prevent persistent prompt injection.
skills/routine-advisor/SKILL.md (81-85)
For consistency with the routine_create tool's updated documentation, which now recommends the standard 5-field cron format (min hour day month weekday), it would be better to update these examples to use 5 fields instead of 6. While the new normalize_cron_expression function handles both, aligning the skill documentation with the tool documentation will improve clarity and predictability for the LLM.
- Daily 9am: `0 9 * * *`
- Weekday mornings: `0 9 * * MON-FRI`
- Weekly Monday: `0 9 * * MON`
- Every 2 hours during work: `0 9-17/2 * * MON-FRI`
- Sunday evening: `0 18 * * SUN`
There was a problem hiding this comment.
Pull request overview
Implements a new first-run experience driven by a proactive “bootstrap greeting” and introduces a psychographic profile pipeline that syncs derived workspace documents and supports routine advising/evolution.
Changes:
- Replace interactive onboarding with BOOTSTRAP.md-based prompt injection + proactive greeting on fresh workspaces.
- Seed workspace identity/memory templates from
src/workspace/seeds/*viainclude_str!instead of hardcoded strings. - Add profile schema/types, profile-to-doc sync (
USER.md, assistant directives, heartbeat), routine-advisor skill, and cron normalization (5/6/7-field support).
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/support/test_rig.rs | Adds bootstrap-aware test rig behavior (keep/clear bootstrap flag; gateway naming). |
| tests/support/test_channel.rs | Adds configurable channel name to support gateway-only bootstrap behavior in tests. |
| tests/fixtures/llm_traces/advanced/bootstrap_greeting.json | Adds an E2E LLM trace fixture for the proactive bootstrap greeting flow. |
| tests/e2e_advanced_traces.rs | Adds an E2E test asserting bootstrap greeting + follow-up interaction. |
| src/workspace/seeds/USER.md | New seed template for user context. |
| src/workspace/seeds/TOOLS.md | New seed template for environment/tool notes. |
| src/workspace/seeds/SOUL.md | New seed template for agent values/boundaries. |
| src/workspace/seeds/README.md | New seed template describing workspace structure. |
| src/workspace/seeds/MEMORY.md | New seed template for long-term memory doc. |
| src/workspace/seeds/IDENTITY.md | New seed template for agent identity doc. |
| src/workspace/seeds/HEARTBEAT.md | New seed template for heartbeat checklist. |
| src/workspace/seeds/BOOTSTRAP.md | New bootstrap instructions used for first-run prompt injection. |
| src/workspace/seeds/AGENTS.md | New seed template for session instructions and profile building guidance. |
| src/workspace/mod.rs | Switches seeds to include_str!, adds bootstrap flags, profile prompt injection tiers, and profile-derived doc sync helpers. |
| src/workspace/document.rs | Adds canonical paths for context/profile.json and assistant directives doc. |
| src/tools/builtin/routine.rs | Normalizes cron expressions before validation/storage; updates schema description to accept 5/6/7-field cron. |
| src/tools/builtin/memory.rs | Removes identity-file write guards; adds profile-write-triggered document sync + onboarding completion persistence. |
| src/setup/wizard.rs | Adds note that personal onboarding is now conversational (bootstrap-driven), not wizard-driven. |
| src/setup/profile_evolution.rs | Adds weekly profile evolution prompt generator + routine prompt template and tests. |
| src/setup/mod.rs | Exposes profile_evolution module and re-exports SetupError. |
| src/setup/README.md | Documents the new personal onboarding flow via bootstrap injection. |
| src/settings.rs | Adds profile_onboarding_completed flag for conversational onboarding completion. |
| src/profile.rs | Introduces psychographic profile types/schema/framework and rendering helpers (USER.md, assistant directives, heartbeat). |
| src/lib.rs | Exposes new profile module. |
| src/app.rs | Loads onboarding-complete flag from settings to suppress repeat bootstrap injection. |
| src/agent/routine.rs | Adds cron normalization helper and expands next_cron_fire to accept 5/6/7-field cron; adds tests. |
| src/agent/agent_loop.rs | Sends a proactive bootstrap greeting at startup when the workspace is freshly seeded; adjusts logging verbosity. |
| skills/routine-advisor/SKILL.md | Adds a routine-advisor skill for suggesting routines based on conversation patterns. |
| skills/delegation/SKILL.md | Adds a delegation skill for task breakdown/tracking workflows. |
| CLAUDE.md | Documents the new src/profile.rs module in the repository layout. |
Comments suppressed due to low confidence (1)
src/tools/builtin/memory.rs:259
memory_writenow allows arbitrary paths without blocking writes to prompt-injected instruction/identity files (e.g. AGENTS.md/SOUL.md/IDENTITY.md/USER.md). This reintroduces a prompt-injection persistence vector (attacker can poison future system prompts). Reintroduce a denylist/allowlist (or require explicit approval) for these files even if they are seeded/system-managed.
path => {
if append {
self.workspace
.append(path, content)
.await
.map_err(|e| ToolError::ExecutionFailed(format!("Write failed: {}", e)))?;
} else {
self.workspace
.write(path, content)
.await
.map_err(|e| ToolError::ExecutionFailed(format!("Write failed: {}", e)))?;
}
path.to_string()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 7. Create TestChannel and ChannelManager. | ||
| let test_channel = Arc::new(TestChannel::new()); | ||
| // When testing bootstrap, the channel must be named "gateway" because | ||
| // the bootstrap greeting targets only the gateway channel. | ||
| let test_channel = if self.keep_bootstrap { | ||
| Arc::new(TestChannel::new().with_name("gateway")) | ||
| } else { | ||
| Arc::new(TestChannel::new()) | ||
| }; |
There was a problem hiding this comment.
build(self) destructures self into locals (including keep_bootstrap), but later the code still references self.keep_bootstrap when choosing the TestChannel name. This won’t compile because self has been moved. Use the local keep_bootstrap variable (or avoid moving self before this point).
There was a problem hiding this comment.
Will check — this may be a non-issue if the destructure happens after the reference, but worth verifying the borrow/move ordering in build().
There was a problem hiding this comment.
Verified — no issue. self is destructured into locals at the top of build(), and by line 659 only those locals are used. keep_bootstrap is a bool (Copy), so there's no borrow/move conflict. The deps and channels values are consumed by Agent::new() before agent is moved into tokio::spawn, and test_channel remains valid as a separate Arc. No code change needed.
| let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(updated_at) else { | ||
| return false; | ||
| }; | ||
| let age = Utc::now().signed_duration_since(parsed); | ||
| // Future timestamps are not "recent" (clock skew / bad data). | ||
| if age.num_seconds() < 0 { | ||
| return false; | ||
| } | ||
| age.num_days() <= max_days |
There was a problem hiding this comment.
is_profile_recent subtracts a DateTime<FixedOffset> (from parse_from_rfc3339) from Utc::now(), which is a different timezone type and won’t compile. Convert parsed to UTC first (e.g., parsed.with_timezone(&Utc)) before computing the age.
There was a problem hiding this comment.
This is actually fine — signed_duration_since accepts any DateTime<Tz> since it's generic over the timezone parameter. DateTime<FixedOffset> works directly without conversion to UTC.
src/agent/agent_loop.rs
Outdated
| let bootstrap_msg = IncomingMessage::new( | ||
| "system", | ||
| "default", | ||
| "Hello! I just set you up. Introduce yourself and help me get started.", | ||
| ); |
There was a problem hiding this comment.
Bootstrap greeting uses IncomingMessage::new("system", ...), but thread persistence (persist_user_message/persist_assistant_response) hard-codes the conversation channel as "gateway", so this will create/persist a second “gateway” conversation under a thread_id resolved for the system channel. To avoid duplicate/ghost threads, create the bootstrap message on the gateway channel and attach the resolved assistant_thread_id as thread_id so session resolution + persistence align.
| let bootstrap_msg = IncomingMessage::new( | |
| "system", | |
| "default", | |
| "Hello! I just set you up. Introduce yourself and help me get started.", | |
| ); | |
| let mut bootstrap_msg = IncomingMessage::new( | |
| "gateway", | |
| "default", | |
| "Hello! I just set you up. Introduce yourself and help me get started.", | |
| ); | |
| if let Some(tid) = &assistant_thread_id { | |
| bootstrap_msg.thread_id = Some(tid.to_string()); | |
| } |
There was a problem hiding this comment.
Valid point about channel alignment. The bootstrap message uses "system" intentionally to distinguish it from real user input in the conversation history, but the persistence path does hardcode "gateway". Will look into aligning the channel or using thread_id attachment as suggested.
There was a problem hiding this comment.
Fixed in 71c0aeb — bootstrap message now uses ("default", "gateway") consistently with the persist and session registration calls. The "system" user was causing the thread ownership mismatch that produced the Rejected write for unavailable thread warning.
There was a problem hiding this comment.
Fixed in f14d136 — removed the duplicate event delegation block and duplicate event listener registrations. Only one delegated data-action click handler remains.
| // Persist the onboarding-completed flag and set the | ||
| // in-memory safety net so BOOTSTRAP.md injection stops | ||
| // even if the LLM forgets to delete it. | ||
| self.workspace.mark_bootstrap_completed(); | ||
| let toml_path = crate::settings::Settings::default_toml_path(); | ||
| if let Ok(Some(mut settings)) = crate::settings::Settings::load_toml(&toml_path) | ||
| && !settings.profile_onboarding_completed | ||
| { | ||
| settings.profile_onboarding_completed = true; | ||
| if let Err(e) = settings.save_toml(&toml_path) { | ||
| tracing::warn!("failed to persist profile_onboarding_completed: {e}"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Writing context/profile.json triggers a tool-side write to the user’s global config.toml (Settings::default_toml_path()). Tool execution should ideally be confined to workspace/db state; persisting host config from an LLM tool call can cause unexpected side effects (and test/CI pollution). Consider moving this persistence to an app-layer component (or store the flag in the DB settings table) instead of writing TOML from the tool.
| // Persist the onboarding-completed flag and set the | |
| // in-memory safety net so BOOTSTRAP.md injection stops | |
| // even if the LLM forgets to delete it. | |
| self.workspace.mark_bootstrap_completed(); | |
| let toml_path = crate::settings::Settings::default_toml_path(); | |
| if let Ok(Some(mut settings)) = crate::settings::Settings::load_toml(&toml_path) | |
| && !settings.profile_onboarding_completed | |
| { | |
| settings.profile_onboarding_completed = true; | |
| if let Err(e) = settings.save_toml(&toml_path) { | |
| tracing::warn!("failed to persist profile_onboarding_completed: {e}"); | |
| } | |
| } | |
| // Mark onboarding as completed in workspace state so | |
| // BOOTSTRAP.md injection stops even if the LLM forgets | |
| // to delete it. Persisting any global config (e.g. | |
| // config.toml) should be handled at the application | |
| // layer, not from within a tool. | |
| self.workspace.mark_bootstrap_completed(); |
There was a problem hiding this comment.
Acknowledged — the config.toml write is a pragmatic choice for now since the settings file is the only durable store available outside the workspace DB. Moving this to an app-layer hook or DB-backed settings table is a reasonable follow-up, but it doesn't block this PR. The write is guarded behind a condition check (only fires once when the flag flips) so test/CI pollution is minimal.
src/settings.rs
Outdated
| /// Reserved flag for whether the conversational profile onboarding has | ||
| /// been completed. | ||
| /// | ||
| /// Intended semantics (not yet wired into the agent loop): this will be | ||
| /// set during the user's first interaction with the running assistant | ||
| /// (not during the setup wizard), after the agent builds a psychographic | ||
| /// profile via `memory_write`. | ||
| /// | ||
| // TODO: Wire into agent loop — currently the system uses bootstrap presence | ||
| // and `is_populated()` to gate onboarding. This flag will eventually | ||
| // distinguish "never onboarded" from "onboarded but profile was reset". |
There was a problem hiding this comment.
The doc comment says profile_onboarding_completed is “not yet wired into the agent loop”, but this PR uses it to suppress BOOTSTRAP injection (loaded in app.rs and checked in Workspace::system_prompt_for_context_inner). Update the comment/TODO to match current behavior so future work doesn’t duplicate or regress it.
| /// Reserved flag for whether the conversational profile onboarding has | |
| /// been completed. | |
| /// | |
| /// Intended semantics (not yet wired into the agent loop): this will be | |
| /// set during the user's first interaction with the running assistant | |
| /// (not during the setup wizard), after the agent builds a psychographic | |
| /// profile via `memory_write`. | |
| /// | |
| // TODO: Wire into agent loop — currently the system uses bootstrap presence | |
| // and `is_populated()` to gate onboarding. This flag will eventually | |
| // distinguish "never onboarded" from "onboarded but profile was reset". | |
| /// Flag for whether the conversational profile onboarding has been | |
| /// completed. | |
| /// | |
| /// Current semantics: this is persisted in settings and used by the | |
| /// agent loop (via the workspace/system-prompt wiring) to suppress | |
| /// BOOTSTRAP/profile-onboarding injection once the user has already | |
| /// completed their conversational onboarding. | |
| /// | |
| /// Future semantics: this should be set during the user's first | |
| /// interaction with the running assistant (not during the setup wizard), | |
| /// after the agent builds a psychographic profile via `memory_write`, | |
| /// and may be extended to distinguish "never onboarded" from "onboarded | |
| /// but profile was reset". |
There was a problem hiding this comment.
Good catch — updated the doc comment in 7cb9f86 to reflect that this flag is now actively wired into the agent loop for suppressing BOOTSTRAP.md injection.
There was a problem hiding this comment.
Fixed in f14d136 — the duplicate listener registration block has been removed.
| "name": "memory_write", | ||
| "arguments": { | ||
| "target": "context/profile.json", | ||
| "content": "{\"preferred_name\":\"Alex\",\"confidence\":0.3,\"cohort\":{\"cohort\":\"unknown\",\"confidence\":30},\"communication\":{\"tone\":\"warm\",\"formality\":\"casual\",\"detail_level\":\"unknown\",\"pace\":\"unknown\",\"response_speed\":\"unknown\",\"decision_making\":\"unknown\"},\"personality\":{\"openness\":50,\"conscientiousness\":50,\"extraversion\":50,\"agreeableness\":50,\"neuroticism\":50,\"resilience\":50,\"creativity\":50,\"analytical\":50,\"empathy\":50},\"behavior\":{\"frictions\":[],\"desired_outcomes\":[],\"pain_points\":[],\"suggested_support\":[]},\"interaction_preferences\":{\"feedback_style\":\"direct\",\"proactivity_style\":\"reactive\"},\"friendship\":{\"style\":\"unknown\",\"qualities\":[]},\"context\":{\"profession\":\"unknown\",\"timezone\":\"unknown\",\"focus_areas\":[]},\"assistance\":{\"proactivity\":\"medium\",\"goals\":[],\"notification_preferences\":\"moderate\"},\"analysis_metadata\":{\"message_count\":2,\"topic_variety\":1},\"schema_version\":2,\"created_at\":\"2026-03-10T00:00:00Z\",\"updated_at\":\"2026-03-10T00:00:00Z\"}" |
There was a problem hiding this comment.
The trace fixture writes context/profile.json with a schema that doesn’t match crate::profile::PsychographicProfile (e.g. uses schema_version, different personality shape). Because memory_write now attempts to parse/sync profile docs and set onboarding-complete based on successful parsing, this fixture should be updated to the current PROFILE_JSON_SCHEMA (including version) to exercise the real bootstrap flow.
| "content": "{\"preferred_name\":\"Alex\",\"confidence\":0.3,\"cohort\":{\"cohort\":\"unknown\",\"confidence\":30},\"communication\":{\"tone\":\"warm\",\"formality\":\"casual\",\"detail_level\":\"unknown\",\"pace\":\"unknown\",\"response_speed\":\"unknown\",\"decision_making\":\"unknown\"},\"personality\":{\"openness\":50,\"conscientiousness\":50,\"extraversion\":50,\"agreeableness\":50,\"neuroticism\":50,\"resilience\":50,\"creativity\":50,\"analytical\":50,\"empathy\":50},\"behavior\":{\"frictions\":[],\"desired_outcomes\":[],\"pain_points\":[],\"suggested_support\":[]},\"interaction_preferences\":{\"feedback_style\":\"direct\",\"proactivity_style\":\"reactive\"},\"friendship\":{\"style\":\"unknown\",\"qualities\":[]},\"context\":{\"profession\":\"unknown\",\"timezone\":\"unknown\",\"focus_areas\":[]},\"assistance\":{\"proactivity\":\"medium\",\"goals\":[],\"notification_preferences\":\"moderate\"},\"analysis_metadata\":{\"message_count\":2,\"topic_variety\":1},\"schema_version\":2,\"created_at\":\"2026-03-10T00:00:00Z\",\"updated_at\":\"2026-03-10T00:00:00Z\"}" | |
| "content": "{\"preferred_name\":\"Alex\",\"confidence\":0.3,\"cohort\":{\"cohort\":\"unknown\",\"confidence\":30},\"communication\":{\"tone\":\"warm\",\"formality\":\"casual\",\"detail_level\":\"unknown\",\"pace\":\"unknown\",\"response_speed\":\"unknown\",\"decision_making\":\"unknown\"},\"personality\":{\"openness\":50,\"conscientiousness\":50,\"extraversion\":50,\"agreeableness\":50,\"neuroticism\":50,\"resilience\":50,\"creativity\":50,\"analytical\":50,\"empathy\":50},\"behavior\":{\"frictions\":[],\"desired_outcomes\":[],\"pain_points\":[],\"suggested_support\":[]},\"interaction_preferences\":{\"feedback_style\":\"direct\",\"proactivity_style\":\"reactive\"},\"friendship\":{\"style\":\"unknown\",\"qualities\":[]},\"context\":{\"profession\":\"unknown\",\"timezone\":\"unknown\",\"focus_areas\":[]},\"assistance\":{\"proactivity\":\"medium\",\"goals\":[],\"notification_preferences\":\"moderate\"},\"analysis_metadata\":{\"message_count\":2,\"topic_variety\":1},\"version\":2,\"created_at\":\"2026-03-10T00:00:00Z\",\"updated_at\":\"2026-03-10T00:00:00Z\"}" |
There was a problem hiding this comment.
Good catch — the fixture uses schema_version while the actual struct expects version. Will update the fixture to match the current PROFILE_JSON_SCHEMA.
There was a problem hiding this comment.
Fixed in 71c0aeb — updated schema_version → version in the fixture to match PROFILE_JSON_SCHEMA in profile.rs.
zmanian
left a comment
There was a problem hiding this comment.
Review: feat: chat onboarding and routine advisor
Verdict: Request changes -- security regression must be addressed before merge.
What It Does
Replaces the interactive setup wizard's onboarding step with a conversational "bootstrap greeting" system:
- Psychographic profiling (
src/profile.rs, 1145 lines) -- 9-dimension analysis framework with personality traits, communication preferences, cohort classification, behavior patterns. Well-designed types with backward-compatible deserializers. - Bootstrap greeting -- Fresh workspaces get a proactive LLM greeting that naturally profiles the user through conversation. Triggered in
agent_loop.rswhenbootstrap_pendingis true. - Seed files -- Identity documents moved from hardcoded strings to
src/workspace/seeds/directory loaded viainclude_str!. Good maintainability improvement. - Profile evolution -- Weekly routine that re-analyzes the profile from recent conversations with confidence gating.
- Skills -- New
delegationandroutine-advisorSKILL.md files.
Critical Issue: Identity File Write Protection Removed
Both Gemini and Copilot flagged this independently. I agree -- this is a blocker.
Commit 2 removes PROTECTED_IDENTITY_FILES and all write checks for IDENTITY.md, SOUL.md, AGENTS.md, and USER.md. These files are injected directly into the system prompt. If an attacker (via prompt injection in tool output) tricks the LLM into calling memory_write(target="SOUL.md", content="[malicious instructions]"), every future session is permanently compromised.
The original code had explicit comments documenting this threat model. The removal has no explanation or replacement mitigation.
Fix options:
- Restore the write protection with an exception for
context/profile.json(which is the legitimate write target for profiling) - If the intent is that the LLM should be able to update these files during onboarding, add a gating mechanism (e.g., only allow writes to identity files during bootstrap, or require user approval)
Secondary Concerns
- Prompt injection in profile fields: Profile data (tone, goals, pain_points) populated from user conversation is rendered into system prompts without sanitization. If a user's early messages contain injection payloads, they persist in the profile and influence all future sessions.
- Unsupervised profile evolution: The weekly evolution routine re-analyzes the profile without user approval or notification. Should at minimum log/audit profile changes.
- PR size: 3,000+ lines across 30 files. The two-commit structure (add
onboarding_chat.rsthen immediately delete it) suggests the PR could have been squashed before submission.
What's Good
- Seed files in
src/workspace/seeds/is a clear improvement over hardcoded strings - Profile type system is well-designed with backward compatibility
- Confidence gating (only inject profile when
> 0.6) prevents premature personalization - The architectural shift from a dedicated onboarding engine to system-prompt-driven flow is sound
profile_onboarding_completedsetting prevents repeat bootstrap
Required Before Merge
- Restore identity file write protection or replace with an equivalent safeguard
- Address profile field sanitization (at minimum, document why it's safe)
Nice to Have
- Squash the two commits to eliminate the add-then-delete of
onboarding_chat.rs - Add tests for profile deserialization edge cases and system prompt injection
- Routine-advisor skill should use 5-field cron format to match tool docs
…nboarding-and-routine-advisor # Conflicts: # tests/support/test_rig.rs
…prompt injection Identity files (SOUL.md, AGENTS.md, USER.md, IDENTITY.md) are injected into every system prompt. Rather than hard-blocking writes (which broke onboarding), scan content through the existing Sanitizer and reject writes with High/Critical severity injection patterns. Medium/Low warnings are logged but allowed. Also clarifies AGENTS.md identity file roles (USER.md = user info, IDENTITY.md = agent identity) and adds IDENTITY.md setup as an explicit bootstrap step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wiring The field is now actively used by the agent loop to suppress BOOTSTRAP.md injection — remove the stale "not yet wired" TODO. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the user authenticates via NEAR AI Cloud API key (option 4), api_key_login() stores the key via set_runtime_env(). But build_nearai_model_fetch_config() was using std::env::var() which doesn't check the runtime overlay — so model listing fell back to session-token auth and re-triggered the interactive NEAR AI authentication menu. Switch to env_or_override() which checks both real env vars and the runtime overlay. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
persist_assistant_response was called with channel="default",
user_id="system" but the assistant thread was created via
get_or_create_assistant_conversation("default", "gateway") which owns
the conversation as user_id="default", channel="gateway". The mismatch
caused ensure_writable_conversation to reject the write with:
WARN Rejected write for unavailable thread id user=system channel=default
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Content-Security-Policy header (added in f48fe95) blocks inline JS via script-src 'self'. All onclick/onchange attributes in index.html are replaced with getElementById().addEventListener() calls. Dynamic inline handlers in app.js (jobs, routines, memory breadcrumb, code blocks, TEE report) are replaced with data-action attributes and a single delegated click handler on document. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…chema field
- Bootstrap IncomingMessage now uses ("default", "gateway") consistently
with persist and session registration calls
- Update bootstrap_greeting.json fixture: schema_version → version to
match current PROFILE_JSON_SCHEMA
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| parts.push(format!( | ||
| "PROFILE JSON SCHEMA:\nWrite to `context/profile.json` using `memory_write` with this exact structure:\n{}\n\n{}\n\n\ | ||
| If the conversation doesn't reveal enough about a dimension, use defaults/unknown.\n\ | ||
| For personality trait scores: 40-60 is average range. Default to 50 if unclear.\n\ | ||
| Only score above 70 or below 30 with strong evidence.", | ||
| crate::profile::ANALYSIS_FRAMEWORK, | ||
| crate::profile::PROFILE_JSON_SCHEMA, | ||
| )); |
There was a problem hiding this comment.
Fixed in 204bd85 — the format string now labels the two sections separately: PROFILE ANALYSIS FRAMEWORK: followed by the framework text, then PROFILE JSON SCHEMA: followed by the target JSON structure. The LLM can now clearly distinguish which blob is descriptive context vs the target schema.
| if self.settings.llm_backend.is_none() { | ||
| if let Ok(b) = std::env::var("LLM_BACKEND") | ||
| && !b.trim().is_empty() | ||
| { | ||
| self.settings.llm_backend = Some(b); | ||
| } else if std::env::var("NEARAI_API_KEY").is_ok() { | ||
| self.settings.llm_backend = Some("nearai".to_string()); | ||
| } else if std::env::var("ANTHROPIC_API_KEY").is_ok() | ||
| || std::env::var("ANTHROPIC_OAUTH_TOKEN").is_ok() | ||
| { | ||
| self.settings.llm_backend = Some("anthropic".to_string()); | ||
| } else if std::env::var("OPENAI_API_KEY").is_ok() { | ||
| self.settings.llm_backend = Some("openai".to_string()); | ||
| } |
There was a problem hiding this comment.
Fixed in 204bd85 — all four std::env::var(...).is_ok() checks in quick-mode backend auto-detection now use .is_ok_and(|v| !v.is_empty()) so empty env vars are not treated as present.
| // Sync derived identity documents when the profile is written. | ||
| let mut synced_docs: Vec<&str> = Vec::new(); | ||
| if path == paths::PROFILE { | ||
| match self.workspace.sync_profile_documents().await { | ||
| Ok(true) => { | ||
| tracing::info!("profile write: synced USER.md + assistant-directives.md"); | ||
| synced_docs.extend_from_slice(&[paths::USER, paths::ASSISTANT_DIRECTIVES]); |
There was a problem hiding this comment.
Fixed in 204bd85 — the target path is now normalized (trim + collapse double slashes) before comparing with paths::PROFILE, so non-canonical variants like context//profile.json still trigger profile sync and onboarding completion.
src/workspace/mod.rs
Outdated
| let has_profile = self | ||
| .read(paths::PROFILE) | ||
| .await | ||
| .is_ok_and(|d| !d.content.trim().is_empty()); |
There was a problem hiding this comment.
Fixed in 204bd85 — seed_if_empty() now requires successful serde_json::from_str::<serde_json::Value>() parse before treating profile.json as populated. Corrupted or non-JSON content no longer suppresses bootstrap seeding. Added regression test seed_if_empty_ignores_corrupted_profile.
…nboarding-and-routine-advisor
…lization, profile validation 1. Label ANALYSIS_FRAMEWORK and PROFILE_JSON_SCHEMA sections separately in bootstrap prompt so the LLM knows which blob is the target structure. 2. Wizard quick-mode backend auto-detection now rejects empty env vars (std::env::var().is_ok_and(|v| !v.is_empty())) to avoid selecting the wrong backend when e.g. NEARAI_API_KEY="" is set. 3. Normalize the target path before comparing with paths::PROFILE in memory_write so non-canonical variants like "context//profile.json" still trigger profile sync. 4. seed_if_empty now requires valid JSON parse of context/profile.json before treating it as a populated profile. Corrupted content no longer permanently suppresses bootstrap seeding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nboarding-and-routine-advisor Resolved conflict in src/tools/execute.rs (kept empty-name early return guard). Updated cron schedule assertion in e2e_builtin_tool_coverage to match staging's store-as-is behavior (6-field, not normalized 7-field).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 40 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/workspace/mod.rs
Outdated
| pub async fn append(&self, path: &str, content: &str) -> Result<(), WorkspaceError> { | ||
| let path = normalize_path(path); | ||
| // Scan system-prompt-injected files for prompt injection. | ||
| if is_system_prompt_file(&path) && !content.is_empty() { | ||
| reject_if_injected(&path, content)?; | ||
| } |
There was a problem hiding this comment.
Fixed in e80203a — Workspace::append() now scans the combined content (doc.content + new chunk) for injection patterns, not just the appended chunk. Split-injection across multiple appends is now caught.
src/workspace/mod.rs
Outdated
| !d.content.trim().is_empty() | ||
| && serde_json::from_str::<serde_json::Value>(&d.content).is_ok() |
There was a problem hiding this comment.
Fixed in e80203a — seed_if_empty() now deserializes into PsychographicProfile instead of serde_json::Value. Stray or legacy JSON that doesn't match the expected schema no longer suppresses bootstrap seeding. Updated existing test fixture to use a valid serialized profile.
src/setup/wizard.rs
Outdated
| // Pre-populate backend from env so step_inference_provider | ||
| // can offer "Keep current provider?" instead of asking from scratch. | ||
| if self.settings.llm_backend.is_none() { | ||
| if let Ok(b) = std::env::var("LLM_BACKEND") | ||
| && !b.trim().is_empty() | ||
| { | ||
| self.settings.llm_backend = Some(b); | ||
| } else if std::env::var("NEARAI_API_KEY").is_ok_and(|v| !v.is_empty()) { |
There was a problem hiding this comment.
Fixed in e80203a — all env var checks now use env_or_override() to honor runtime overlays and injected secrets. The LLM_BACKEND value is trimmed before storage to prevent trailing whitespace issues.
…nboarding-and-routine-advisor
…_override 1. Workspace::append() now scans the combined content (existing + new) for prompt injection, not just the appended chunk. Prevents split- injection evasion across multiple appends. 2. seed_if_empty() now deserializes into PsychographicProfile instead of serde_json::Value for profile validation. Stray/legacy JSON that doesn't match the expected schema no longer suppresses bootstrap. 3. Wizard quick-mode backend auto-detection now uses env_or_override() to honor runtime overlays and injected secrets. LLM_BACKEND value is trimmed before storage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exercises the full onboarding flow end-to-end: 1. Bootstrap greeting fires automatically on fresh workspace 2. User converses for 3 turns (name, tools, work style) 3. Agent writes psychographic profile to context/profile.json 4. Profile sync generates USER.md and assistant-directives.md 5. Agent writes IDENTITY.md (chosen persona) 6. Agent clears BOOTSTRAP.md via memory_write(target: "bootstrap") Verifies: - BOOTSTRAP.md is non-empty before onboarding, empty after - bootstrap_completed flag is set - Profile contains expected user data (name, profession, interests) - USER.md contains profile-derived content (name, tone, profession) - Assistant-directives.md references user and communication style - IDENTITY.md contains agent's chosen persona name - All memory_write calls succeed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Sync derived identity documents when the profile is written. | ||
| // Normalize the path for comparison: trim slashes and collapse doubles, | ||
| // matching the normalization Workspace::write() applies internally. | ||
| let normalized_path = path.trim().trim_matches('/').replace("//", "/"); | ||
| let mut synced_docs: Vec<&str> = Vec::new(); | ||
| if normalized_path == paths::PROFILE { | ||
| match self.workspace.sync_profile_documents().await { | ||
| Ok(true) => { |
There was a problem hiding this comment.
normalized_path normalization here doesn’t actually match Workspace::normalize_path() (it only does a single replace("//", "/"), which won’t fully collapse runs like "context///profile.json"). In that case the workspace write will still hit context/profile.json, but this comparison can fail and skip profile-sync + onboarding flag updates. Consider extracting/shared-normalizing with the same collapse loop as Workspace uses (or otherwise ensure full slash-collapsing + trim) before comparing to paths::PROFILE.
There was a problem hiding this comment.
Fixed in 8473b2a — path normalization now uses the same char-by-char slash-collapsing loop as Workspace::normalize_path(), so runs like context///profile.json are fully collapsed before comparison.
src/setup/wizard.rs
Outdated
| if let Ok(api_key) = std::env::var("NEARAI_API_KEY") | ||
| && !api_key.is_empty() | ||
| && self.settings.llm_backend.as_deref() == Some("nearai") | ||
| { |
There was a problem hiding this comment.
Quick-mode detection uses std::env::var("NEARAI_API_KEY"), which bypasses the project’s env_or_override() runtime overlay mechanism used elsewhere in this function. This means API keys injected via runtime overrides (and the “empty treated as unset” behavior) won’t be honored and quick-mode may unexpectedly fall back to interactive prompts. Use env_or_override("NEARAI_API_KEY") (and the same trimming/empty handling) here as well.
| if let Ok(api_key) = std::env::var("NEARAI_API_KEY") | |
| && !api_key.is_empty() | |
| && self.settings.llm_backend.as_deref() == Some("nearai") | |
| { | |
| if let Some(api_key) = { | |
| use crate::config::helpers::env_or_override; | |
| env_or_override("NEARAI_API_KEY") | |
| } | |
| && !api_key.trim().is_empty() | |
| && self.settings.llm_backend.as_deref() == Some("nearai") | |
| { | |
| let api_key = api_key.trim().to_string(); |
There was a problem hiding this comment.
Fixed in 8473b2a — the NEARAI_API_KEY check at line 239 now uses env_or_override() consistently with the backend auto-detection block above it.
src/agent/routine.rs
Outdated
| let fields: Vec<&str> = schedule.split_whitespace().collect(); | ||
| match fields.len() { | ||
| 5 => format!("0 {} *", schedule.trim()), | ||
| 6 => format!("{} *", schedule.trim()), | ||
| _ => schedule.to_string(), |
There was a problem hiding this comment.
normalize_cron_expression() trims the input for 5/6-field cases, but for 7-field (or any other) it returns schedule.to_string() without trimming. If a user-supplied schedule contains leading/trailing whitespace/newlines, next_cron_fire() will pass that through to cron::Schedule::from_str and may reject an otherwise-valid cron. Consider returning schedule.trim().to_string() in the passthrough branch (and/or trimming before counting fields) to make normalization consistent.
| let fields: Vec<&str> = schedule.split_whitespace().collect(); | |
| match fields.len() { | |
| 5 => format!("0 {} *", schedule.trim()), | |
| 6 => format!("{} *", schedule.trim()), | |
| _ => schedule.to_string(), | |
| let trimmed = schedule.trim(); | |
| let fields: Vec<&str> = trimmed.split_whitespace().collect(); | |
| match fields.len() { | |
| 5 => format!("0 {} *", trimmed), | |
| 6 => format!("{} *", trimmed), | |
| _ => trimmed.to_string(), |
There was a problem hiding this comment.
Fixed in 8473b2a — normalize_cron_expression() now trims input before field counting, so the passthrough branch (7+ fields) also strips leading/trailing whitespace.
…rim [skip-regression-check] 1. memory.rs path normalization now uses the same char-by-char loop as Workspace::normalize_path() to fully collapse consecutive slashes (e.g. "context///profile.json" → "context/profile.json"). 2. Quick-mode NEARAI_API_KEY check (line 239) now uses env_or_override() consistently with the backend auto-detection block above it. 3. normalize_cron_expression() trims input before field counting so the passthrough branch (7+ fields) also strips whitespace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code reviewFound 8 issues:
Confidence threshold 0.6 scattered across workspace/mod.rs and setup/profile_evolution.rs. If requirements change, multiple files must be manually updated. Add const PROFILE_CONFIDENCE_THRESHOLD: f64 = 0.6 and use consistently. Lines 809 to 815 in 806d402
PersonalityTraits, CommunicationPreferences use String instead of enums (detail_level, formality as String with literal values). No type safety or exhaustiveness checking. Consider enum types with serde for backward compatibility. Lines 150 to 200 in 806d402
deserialize_trait_score() uses unwrap_or(50.0) which converts deserialization errors to silent defaults. Corrupted profile.json with non-numeric values will silently use 50. Should propagate with ? operator. Lines 120 to 128 in 806d402
Lines 104-128 iterate warnings 3 times (any check, filter/map collect, for loop log). Could combine into single pass. Lines 101 to 130 in 806d402
merge_profile_section() matches hardcoded strings like 'User Context' and 'Name:'. Fragile approach; consider versioned format or metadata. Lines 900 to 950 in 806d402
Profile deserialized twice (build_system_prompt and seed_if_empty). With large profiles (>10KB), repeated parsing on every message. Cache or parse once. Lines 809 to 815 in 806d402
agent/agent_loop.rs line 729 silently ignores broadcast errors. Bootstrap persists but won't notify clients if SSE fails. Should log warning. ironclaw/src/agent/agent_loop.rs Line 729 in 806d402
seed_if_empty() treats valid JSON as populated even if missing required fields. Stricter validation recommended. Lines 1180 to 1190 in 806d402 |
* feat: port NPA psychographic profiling system into IronClaw
Port the complete psychographic profiling system from NPA into IronClaw,
including enriched profile schema, conversational onboarding, profile
evolution, and three-tier prompt augmentation.
Personal onboarding moved from wizard Step 9 to first assistant
interaction per maintainer feedback — the First Contact system prompt
block now instructs the LLM to conduct a natural onboarding conversation
that builds the psychographic profile via memory_write.
Changes:
- Enrich profile.rs with 5 new structs, 9-dimension analysis framework,
custom deserializers for backward compatibility, and rendering methods
- Add conversational onboarding engine with one-step-removed questioning
technique, personality framework, and confidence-scored profile generation
- Add profile evolution with confidence gating, analysis metadata tracking,
and weekly update routine
- Replace thin interaction style injection with three-tier system gated on
confidence > 0.6 and profile recency
- Replace wizard Step 9 with First Contact system prompt block that drives
conversational onboarding during the user's first interaction
- Add autonomy progression to SOUL.md seed and personality framework to
AGENTS.md seed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace chat-based onboarding with bootstrap greeting and workspace seeds
Remove the interactive onboarding_chat.rs engine in favor of a simpler
bootstrap flow: fresh workspaces get a proactive LLM greeting that
naturally profiles the user. Identity files are now seeded from
src/workspace/seeds/ instead of being hardcoded. Also removes the
identity-file write protection (seeds are now managed), adds routine
advisor integration, and includes an e2e trace for bootstrap greeting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(safety): sanitize identity file writes via Sanitizer to prevent prompt injection
Identity files (SOUL.md, AGENTS.md, USER.md, IDENTITY.md) are injected into
every system prompt. Rather than hard-blocking writes (which broke onboarding),
scan content through the existing Sanitizer and reject writes with High/Critical
severity injection patterns. Medium/Low warnings are logged but allowed.
Also clarifies AGENTS.md identity file roles (USER.md = user info, IDENTITY.md =
agent identity) and adds IDENTITY.md setup as an explicit bootstrap step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update profile_onboarding_completed comment to reflect current wiring
The field is now actively used by the agent loop to suppress BOOTSTRAP.md
injection — remove the stale "not yet wired" TODO.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(setup): use env_or_override for NEARAI_API_KEY in model fetch config
When the user authenticates via NEAR AI Cloud API key (option 4),
api_key_login() stores the key via set_runtime_env(). But
build_nearai_model_fetch_config() was using std::env::var() which
doesn't check the runtime overlay — so model listing fell back to
session-token auth and re-triggered the interactive NEAR AI
authentication menu.
Switch to env_or_override() which checks both real env vars and the
runtime overlay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(agent): correct channel/user_id in bootstrap greeting persist call
persist_assistant_response was called with channel="default",
user_id="system" but the assistant thread was created via
get_or_create_assistant_conversation("default", "gateway") which owns
the conversation as user_id="default", channel="gateway". The mismatch
caused ensure_writable_conversation to reject the write with:
WARN Rejected write for unavailable thread id user=system channel=default
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): remove all inline event handlers for CSP compliance
The Content-Security-Policy header (added in f48fe95) blocks inline JS
via script-src 'self'. All onclick/onchange attributes in index.html
are replaced with getElementById().addEventListener() calls. Dynamic
inline handlers in app.js (jobs, routines, memory breadcrumb, code
blocks, TEE report) are replaced with data-action attributes and a
single delegated click handler on document.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(agent): align bootstrap message user/channel and update fixture schema field
- Bootstrap IncomingMessage now uses ("default", "gateway") consistently
with persist and session registration calls
- Update bootstrap_greeting.json fixture: schema_version → version to
match current PROFILE_JSON_SCHEMA
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(safety): address PR review — expand injection scanning and harden profile sync
- BOOTSTRAP.md: fix target "profile" → "context/profile.json" so the
write hits the correct path and triggers profile sync
- IDENTITY_FILES: add context/assistant-directives.md to the scanned
set since it is also injected into the system prompt
- sync_profile_documents(): scan derived USER.md and assistant-directives
content through Sanitizer before writing, rejecting High/Critical
injection patterns
- profile_evolution_prompt(): wrap recent_messages_summary in <user_data>
delimiters with untrusted-data instruction to mitigate indirect
prompt injection
- routine-advisor skill: update cron examples from 6-field to standard
5-field format for consistency with routine_create tool docs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(setup): detect env-provided LLM keys during quick-mode onboarding
Quick-mode wizard now checks LLM_BACKEND, NEARAI_API_KEY,
ANTHROPIC_API_KEY, and OPENAI_API_KEY env vars to pre-populate
the provider setting, so users aren't re-prompted for credentials
they already supplied. Also teaches setup_nearai() to recognize
NEARAI_API_KEY from env (previously only checked session tokens).
Includes web UI cleanup (remove duplicate event listeners) and
e2e test response count adjustment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): update routine_create_list to expect 7-field normalized cron
The cron normalizer now always expands to 7-field format, so the
stored schedule is "0 0 9 * * * *" not "0 0 9 * * *".
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(setup): skip LLM provider prompts when NEARAI_API_KEY is present
In quick mode, if NEARAI_API_KEY is set in the environment and the
backend was auto-detected as nearai, skip the interactive inference
provider and model selection steps. The API key is persisted to the
secrets store and a default model is set automatically.
Also simplify the static fallback model list for nearai to a single
default entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: unify default model, static bootstrap greeting, and web UI cleanup
- Add DEFAULT_MODEL const and default_models() fallback list in
llm/nearai_chat.rs; use from config, wizard, and .env.example so the
default model is defined in one place
- Restore multi-model fallback list in setup wizard (was reduced to 1)
- Move BOOTSTRAP_GREETING to module-level const (out of run() body)
- Replace LLM-based bootstrap with static greeting (persist to DB before
channels start, then broadcast — eliminates startup LLM call and race)
- Fix double env::var read for NEARAI_API_KEY in quick setup path
- Move thread sidebar buttons into threads-section-header (web UI)
- Remove orphaned .thread-sidebar-header CSS and fix double blank line
- Update bootstrap e2e test for static greeting (no LLM trace needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(safety): move prompt injection scanning into Workspace write/append
Addresses PR #927 review comments (#1, #3) — identity file write
protection and unsanitized profile fields in system prompt.
Instead of scanning at the tool layer (memory.rs) or the sync layer
(sync_profile_documents), injection scanning now lives in
Workspace::write() and Workspace::append() for all files that are
injected into the system prompt. This ensures every code path that
writes to these files is protected, including future ones.
- Add SYSTEM_PROMPT_FILES const and reject_if_injected() in workspace
- Add WorkspaceError::InjectionRejected variant
- Add map_write_err() in memory.rs to convert InjectionRejected to
ToolError::NotAuthorized
- Remove redundant IDENTITY_FILES/Sanitizer from memory.rs
- Remove redundant sanitizer calls from sync_profile_documents()
- Move sanitization tests to workspace::tests
- Existing integration test (test_memory_write_rejects_injection)
continues to pass through the new path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — merge marker order, orphan thread, stale fixture
- merge_profile_section: search for END marker after BEGIN position to
avoid matching a stray END earlier in the file
- Bootstrap phase 2: use get_or_create_session + Thread::with_id instead
of resolve_thread(None) to avoid creating an orphan thread
- setup_nearai: use env_or_override for NEARAI_API_KEY consistency with
runtime overlay
- Delete orphaned bootstrap_greeting.json fixture (no test references it)
- Add test_merge_end_marker_must_follow_begin regression test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fmt agent_loop.rs (CI stable rustfmt)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lazy-init sanitizer, check profile non-empty before skipping bootstrap
Address Copilot review:
- Use LazyLock<Sanitizer> to avoid rebuilding Aho-Corasick + regexes
on every workspace write
- has_profile check now requires non-empty content, not just file
existence, to prevent empty profile.json from suppressing onboarding
- Add seed_tests integration tests (libsql-backed) verifying:
- Empty profile.json does not suppress BOOTSTRAP.md seeding
- Non-empty profile.json correctly suppresses bootstrap for upgrades
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: duplicate language handler, empty LLM_BACKEND, test_rig style
Address Copilot review on PR #927:
- Remove duplicate language-option click listeners (delegated
data-action handler already covers them)
- Guard LLM_BACKEND env prefill against empty string to prevent
suppressing API-key-based auto-detection
- Use destructured local `keep_bootstrap` instead of `self.keep_bootstrap`
in test_rig for consistency after destructure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update stale BOOTSTRAP.md write-protection comment [skip-regression-check]
BOOTSTRAP.md is now in SYSTEM_PROMPT_FILES and gets injection scanning
on write. The old comment incorrectly stated it was not write-protected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace debug_assert panics with graceful error returns [skip-regression-check]
debug_assert! in execute_tool_with_safety and JobContext::transition_to
panicked in test builds before the graceful error path could run.
Existing tests (test_cancel_job_completed, test_execute_empty_tool_name_returns_not_found)
already cover these paths — they were the ones failing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — schema label, env var check, path normalization, profile validation
1. Label ANALYSIS_FRAMEWORK and PROFILE_JSON_SCHEMA sections separately
in bootstrap prompt so the LLM knows which blob is the target structure.
2. Wizard quick-mode backend auto-detection now rejects empty env vars
(std::env::var().is_ok_and(|v| !v.is_empty())) to avoid selecting the
wrong backend when e.g. NEARAI_API_KEY="" is set.
3. Normalize the target path before comparing with paths::PROFILE in
memory_write so non-canonical variants like "context//profile.json"
still trigger profile sync.
4. seed_if_empty now requires valid JSON parse of context/profile.json
before treating it as a populated profile. Corrupted content no longer
permanently suppresses bootstrap seeding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
* fix: address Copilot review — append scan, profile validation, env_or_override
1. Workspace::append() now scans the combined content (existing + new)
for prompt injection, not just the appended chunk. Prevents split-
injection evasion across multiple appends.
2. seed_if_empty() now deserializes into PsychographicProfile instead of
serde_json::Value for profile validation. Stray/legacy JSON that
doesn't match the expected schema no longer suppresses bootstrap.
3. Wizard quick-mode backend auto-detection now uses env_or_override()
to honor runtime overlays and injected secrets. LLM_BACKEND value
is trimmed before storage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add bootstrap_onboarding_clears_bootstrap E2E trace test
Exercises the full onboarding flow end-to-end:
1. Bootstrap greeting fires automatically on fresh workspace
2. User converses for 3 turns (name, tools, work style)
3. Agent writes psychographic profile to context/profile.json
4. Profile sync generates USER.md and assistant-directives.md
5. Agent writes IDENTITY.md (chosen persona)
6. Agent clears BOOTSTRAP.md via memory_write(target: "bootstrap")
Verifies:
- BOOTSTRAP.md is non-empty before onboarding, empty after
- bootstrap_completed flag is set
- Profile contains expected user data (name, profession, interests)
- USER.md contains profile-derived content (name, tone, profession)
- Assistant-directives.md references user and communication style
- IDENTITY.md contains agent's chosen persona name
- All memory_write calls succeed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — slash collapse, env_or_override, cron trim [skip-regression-check]
1. memory.rs path normalization now uses the same char-by-char loop as
Workspace::normalize_path() to fully collapse consecutive slashes
(e.g. "context///profile.json" → "context/profile.json").
2. Quick-mode NEARAI_API_KEY check (line 239) now uses env_or_override()
consistently with the backend auto-detection block above it.
3. normalize_cron_expression() trims input before field counting so the
passthrough branch (7+ fields) also strips whitespace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jay Zalowitz <jayzalowitz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: port NPA psychographic profiling system into IronClaw
Port the complete psychographic profiling system from NPA into IronClaw,
including enriched profile schema, conversational onboarding, profile
evolution, and three-tier prompt augmentation.
Personal onboarding moved from wizard Step 9 to first assistant
interaction per maintainer feedback — the First Contact system prompt
block now instructs the LLM to conduct a natural onboarding conversation
that builds the psychographic profile via memory_write.
Changes:
- Enrich profile.rs with 5 new structs, 9-dimension analysis framework,
custom deserializers for backward compatibility, and rendering methods
- Add conversational onboarding engine with one-step-removed questioning
technique, personality framework, and confidence-scored profile generation
- Add profile evolution with confidence gating, analysis metadata tracking,
and weekly update routine
- Replace thin interaction style injection with three-tier system gated on
confidence > 0.6 and profile recency
- Replace wizard Step 9 with First Contact system prompt block that drives
conversational onboarding during the user's first interaction
- Add autonomy progression to SOUL.md seed and personality framework to
AGENTS.md seed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace chat-based onboarding with bootstrap greeting and workspace seeds
Remove the interactive onboarding_chat.rs engine in favor of a simpler
bootstrap flow: fresh workspaces get a proactive LLM greeting that
naturally profiles the user. Identity files are now seeded from
src/workspace/seeds/ instead of being hardcoded. Also removes the
identity-file write protection (seeds are now managed), adds routine
advisor integration, and includes an e2e trace for bootstrap greeting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(safety): sanitize identity file writes via Sanitizer to prevent prompt injection
Identity files (SOUL.md, AGENTS.md, USER.md, IDENTITY.md) are injected into
every system prompt. Rather than hard-blocking writes (which broke onboarding),
scan content through the existing Sanitizer and reject writes with High/Critical
severity injection patterns. Medium/Low warnings are logged but allowed.
Also clarifies AGENTS.md identity file roles (USER.md = user info, IDENTITY.md =
agent identity) and adds IDENTITY.md setup as an explicit bootstrap step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update profile_onboarding_completed comment to reflect current wiring
The field is now actively used by the agent loop to suppress BOOTSTRAP.md
injection — remove the stale "not yet wired" TODO.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(setup): use env_or_override for NEARAI_API_KEY in model fetch config
When the user authenticates via NEAR AI Cloud API key (option 4),
api_key_login() stores the key via set_runtime_env(). But
build_nearai_model_fetch_config() was using std::env::var() which
doesn't check the runtime overlay — so model listing fell back to
session-token auth and re-triggered the interactive NEAR AI
authentication menu.
Switch to env_or_override() which checks both real env vars and the
runtime overlay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(agent): correct channel/user_id in bootstrap greeting persist call
persist_assistant_response was called with channel="default",
user_id="system" but the assistant thread was created via
get_or_create_assistant_conversation("default", "gateway") which owns
the conversation as user_id="default", channel="gateway". The mismatch
caused ensure_writable_conversation to reject the write with:
WARN Rejected write for unavailable thread id user=system channel=default
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): remove all inline event handlers for CSP compliance
The Content-Security-Policy header (added in f48fe95) blocks inline JS
via script-src 'self'. All onclick/onchange attributes in index.html
are replaced with getElementById().addEventListener() calls. Dynamic
inline handlers in app.js (jobs, routines, memory breadcrumb, code
blocks, TEE report) are replaced with data-action attributes and a
single delegated click handler on document.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(agent): align bootstrap message user/channel and update fixture schema field
- Bootstrap IncomingMessage now uses ("default", "gateway") consistently
with persist and session registration calls
- Update bootstrap_greeting.json fixture: schema_version → version to
match current PROFILE_JSON_SCHEMA
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(safety): address PR review — expand injection scanning and harden profile sync
- BOOTSTRAP.md: fix target "profile" → "context/profile.json" so the
write hits the correct path and triggers profile sync
- IDENTITY_FILES: add context/assistant-directives.md to the scanned
set since it is also injected into the system prompt
- sync_profile_documents(): scan derived USER.md and assistant-directives
content through Sanitizer before writing, rejecting High/Critical
injection patterns
- profile_evolution_prompt(): wrap recent_messages_summary in <user_data>
delimiters with untrusted-data instruction to mitigate indirect
prompt injection
- routine-advisor skill: update cron examples from 6-field to standard
5-field format for consistency with routine_create tool docs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: cargo fmt
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(setup): detect env-provided LLM keys during quick-mode onboarding
Quick-mode wizard now checks LLM_BACKEND, NEARAI_API_KEY,
ANTHROPIC_API_KEY, and OPENAI_API_KEY env vars to pre-populate
the provider setting, so users aren't re-prompted for credentials
they already supplied. Also teaches setup_nearai() to recognize
NEARAI_API_KEY from env (previously only checked session tokens).
Includes web UI cleanup (remove duplicate event listeners) and
e2e test response count adjustment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): update routine_create_list to expect 7-field normalized cron
The cron normalizer now always expands to 7-field format, so the
stored schedule is "0 0 9 * * * *" not "0 0 9 * * *".
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(setup): skip LLM provider prompts when NEARAI_API_KEY is present
In quick mode, if NEARAI_API_KEY is set in the environment and the
backend was auto-detected as nearai, skip the interactive inference
provider and model selection steps. The API key is persisted to the
secrets store and a default model is set automatically.
Also simplify the static fallback model list for nearai to a single
default entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: unify default model, static bootstrap greeting, and web UI cleanup
- Add DEFAULT_MODEL const and default_models() fallback list in
llm/nearai_chat.rs; use from config, wizard, and .env.example so the
default model is defined in one place
- Restore multi-model fallback list in setup wizard (was reduced to 1)
- Move BOOTSTRAP_GREETING to module-level const (out of run() body)
- Replace LLM-based bootstrap with static greeting (persist to DB before
channels start, then broadcast — eliminates startup LLM call and race)
- Fix double env::var read for NEARAI_API_KEY in quick setup path
- Move thread sidebar buttons into threads-section-header (web UI)
- Remove orphaned .thread-sidebar-header CSS and fix double blank line
- Update bootstrap e2e test for static greeting (no LLM trace needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(safety): move prompt injection scanning into Workspace write/append
Addresses PR #927 review comments (#1, #3) — identity file write
protection and unsanitized profile fields in system prompt.
Instead of scanning at the tool layer (memory.rs) or the sync layer
(sync_profile_documents), injection scanning now lives in
Workspace::write() and Workspace::append() for all files that are
injected into the system prompt. This ensures every code path that
writes to these files is protected, including future ones.
- Add SYSTEM_PROMPT_FILES const and reject_if_injected() in workspace
- Add WorkspaceError::InjectionRejected variant
- Add map_write_err() in memory.rs to convert InjectionRejected to
ToolError::NotAuthorized
- Remove redundant IDENTITY_FILES/Sanitizer from memory.rs
- Remove redundant sanitizer calls from sync_profile_documents()
- Move sanitization tests to workspace::tests
- Existing integration test (test_memory_write_rejects_injection)
continues to pass through the new path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — merge marker order, orphan thread, stale fixture
- merge_profile_section: search for END marker after BEGIN position to
avoid matching a stray END earlier in the file
- Bootstrap phase 2: use get_or_create_session + Thread::with_id instead
of resolve_thread(None) to avoid creating an orphan thread
- setup_nearai: use env_or_override for NEARAI_API_KEY consistency with
runtime overlay
- Delete orphaned bootstrap_greeting.json fixture (no test references it)
- Add test_merge_end_marker_must_follow_begin regression test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fmt agent_loop.rs (CI stable rustfmt)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lazy-init sanitizer, check profile non-empty before skipping bootstrap
Address Copilot review:
- Use LazyLock<Sanitizer> to avoid rebuilding Aho-Corasick + regexes
on every workspace write
- has_profile check now requires non-empty content, not just file
existence, to prevent empty profile.json from suppressing onboarding
- Add seed_tests integration tests (libsql-backed) verifying:
- Empty profile.json does not suppress BOOTSTRAP.md seeding
- Non-empty profile.json correctly suppresses bootstrap for upgrades
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: duplicate language handler, empty LLM_BACKEND, test_rig style
Address Copilot review on PR #927:
- Remove duplicate language-option click listeners (delegated
data-action handler already covers them)
- Guard LLM_BACKEND env prefill against empty string to prevent
suppressing API-key-based auto-detection
- Use destructured local `keep_bootstrap` instead of `self.keep_bootstrap`
in test_rig for consistency after destructure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update stale BOOTSTRAP.md write-protection comment [skip-regression-check]
BOOTSTRAP.md is now in SYSTEM_PROMPT_FILES and gets injection scanning
on write. The old comment incorrectly stated it was not write-protected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace debug_assert panics with graceful error returns [skip-regression-check]
debug_assert! in execute_tool_with_safety and JobContext::transition_to
panicked in test builds before the graceful error path could run.
Existing tests (test_cancel_job_completed, test_execute_empty_tool_name_returns_not_found)
already cover these paths — they were the ones failing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — schema label, env var check, path normalization, profile validation
1. Label ANALYSIS_FRAMEWORK and PROFILE_JSON_SCHEMA sections separately
in bootstrap prompt so the LLM knows which blob is the target structure.
2. Wizard quick-mode backend auto-detection now rejects empty env vars
(std::env::var().is_ok_and(|v| !v.is_empty())) to avoid selecting the
wrong backend when e.g. NEARAI_API_KEY="" is set.
3. Normalize the target path before comparing with paths::PROFILE in
memory_write so non-canonical variants like "context//profile.json"
still trigger profile sync.
4. seed_if_empty now requires valid JSON parse of context/profile.json
before treating it as a populated profile. Corrupted content no longer
permanently suppresses bootstrap seeding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: cargo fmt
* fix: address Copilot review — append scan, profile validation, env_or_override
1. Workspace::append() now scans the combined content (existing + new)
for prompt injection, not just the appended chunk. Prevents split-
injection evasion across multiple appends.
2. seed_if_empty() now deserializes into PsychographicProfile instead of
serde_json::Value for profile validation. Stray/legacy JSON that
doesn't match the expected schema no longer suppresses bootstrap.
3. Wizard quick-mode backend auto-detection now uses env_or_override()
to honor runtime overlays and injected secrets. LLM_BACKEND value
is trimmed before storage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add bootstrap_onboarding_clears_bootstrap E2E trace test
Exercises the full onboarding flow end-to-end:
1. Bootstrap greeting fires automatically on fresh workspace
2. User converses for 3 turns (name, tools, work style)
3. Agent writes psychographic profile to context/profile.json
4. Profile sync generates USER.md and assistant-directives.md
5. Agent writes IDENTITY.md (chosen persona)
6. Agent clears BOOTSTRAP.md via memory_write(target: "bootstrap")
Verifies:
- BOOTSTRAP.md is non-empty before onboarding, empty after
- bootstrap_completed flag is set
- Profile contains expected user data (name, profession, interests)
- USER.md contains profile-derived content (name, tone, profession)
- Assistant-directives.md references user and communication style
- IDENTITY.md contains agent's chosen persona name
- All memory_write calls succeed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Copilot review — slash collapse, env_or_override, cron trim [skip-regression-check]
1. memory.rs path normalization now uses the same char-by-char loop as
Workspace::normalize_path() to fully collapse consecutive slashes
(e.g. "context///profile.json" → "context/profile.json").
2. Quick-mode NEARAI_API_KEY check (line 239) now uses env_or_override()
consistently with the backend auto-detection block above it.
3. normalize_cron_expression() trims input before field counting so the
passthrough branch (7+ fields) also strips whitespace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jay Zalowitz <jayzalowitz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
onboarding_chat.rsenginesrc/workspace/seeds/instead of hardcoded stringsNEARAI_API_KEY,ANTHROPIC_API_KEY,OPENAI_API_KEY, andLLM_BACKENDenv vars, skipping redundant provider prompts during onboardingSecurity (addressed in review)
Sanitizer::detect()scanningsync_profile_documents()sanitized before writing to system-prompt-injected files<user_data>delimiterscontext/assistant-directives.mdadded to injection-scanned file setBuilds on the NPA psychographic profiling work from #321.
Test plan
cargo fmt— no formatting issuescargo clippy --all --benches --tests --examples --all-features— zero warningscargo test— unit tests passNEARAI_API_KEYin env skips provider prompt during onboardingCloses #321
🤖 Generated with Claude Code